Storage Transfer ServiceがS3へアクセスするときのIP範囲の変更検知方法を考えてみた
概要
Storage Transfer Service(以下STS)がAmazon S3にアクセスする際のアクセス元IPアドレスの範囲は2023/4/25より公開されています。
このIPアドレスの範囲をS3のバケットポリシーに設定することでSTSと連携するバケットにIP制限を設定することができます。
ただ、公開されているIPアドレスは変更される可能性があるとリファレンスに記載があります。
これらのIP範囲は変更される可能性があるため、永続的なアドレスに現在の値を JSON ファイルとして公開します。
https://www.gstatic.com/storage-transfer-service/ipranges.json
ファイルに新しい範囲が追加される場合、Storage Transfer Service からのリクエストに対してその範囲が使用されるまで少なくとも 7 日間は待機します。
セキュリティ構成を最新の状態に保つため、少なくとも週に 1 回、このドキュメントからデータを pull することをおすすめします。JSON ファイルから IP 範囲を取得する Python スクリプトの例については、Virtual Private Cloud のドキュメントをご覧ください。
引用:https://cloud.google.com/storage-transfer/docs/source-amazon-s3?hl=ja#ip_restrictions
ポイントは2点あると考えます。
- IP範囲はこちらでJSONファイルとして公開されています
- IP範囲が追加された場合、STSがS3へのリクエストに追加されたIP範囲を使用するまで少なくとも7日間ある
つまりは、公開されているIP範囲のJSONを取得して設定済みのIP範囲と差分があるかどうかチェックする仕組みを実装すればIP範囲の追加を見逃さずに済むということだと考えます。
(理想はIP範囲が追加されたらバケットポリシーに追加するという仕組みかもですが)
ただ今のところ2023/4/25に公開された時からIP範囲は追加されていないようなのでとりあえずはチェックする仕組みだけ実装してみようかなと思いました。
※いつ追加されるかはわからないので当然注意が必要です。
というわけで公開されているIP範囲のJSONを取得して、差分チェックして差分を検知する仕組みを考えてみました。よかったら読んでみてください。
やってみる
検知する仕組みのイメージは以下となります。
- Cloud Run FunctionsでIP範囲のJSONを取得
- 初回実行ならFirestoreにそのまま保存
- 初回以外ならFirestoreのIP範囲とJSONで取得したIP範囲を比較
- 追加・削除があればログ出力(通知)
という流れです。これをCloud Run Functionsで実装してみました。
※Firestoreは設定済みの前提です。
とりあえずスクリプト全文です
import functions_framework
import json
import requests
from google.cloud import firestore
from datetime import datetime
# Firestoreコレクションとドキュメントの設定
COLLECTION_NAME = 'ip_ranges'
DOCUMENT_NAME = 'current'
# STSのIP範囲公開JSON URL
GCS_IP_RANGES_URL = 'https://www.gstatic.com/storage-transfer-service/ipranges.json'
def filter_data(data):
"""比較対象のデータから不要なフィールドを除外"""
if data is None:
return None
return {
"syncToken": data.get("syncToken"),
"prefixes": data.get("prefixes")
}
def find_differences(old_prefixes, new_prefixes):
"""新旧の prefixes を比較して差分を見つける"""
old_set = set(json.dumps(prefix, sort_keys=True) for prefix in old_prefixes)
new_set = set(json.dumps(prefix, sort_keys=True) for prefix in new_prefixes)
added = new_set - old_set
removed = old_set - new_set
return {
"added": [json.loads(item) for item in added],
"removed": [json.loads(item) for item in removed]
}
@functions_framework.http
def detect_gcs_ip_changes(request):
try:
# STSのIP範囲取得
response = requests.get(GCS_IP_RANGES_URL)
response.raise_for_status()
new_ip_ranges = response.json()
db = firestore.Client()
doc_ref = db.collection(COLLECTION_NAME).document(DOCUMENT_NAME)
# Firestoreから設定済みIP範囲取得
doc_snapshot = doc_ref.get()
if doc_snapshot.exists:
old_ip_ranges = doc_snapshot.to_dict()
else:
old_ip_ranges = None
# 差分があるかどうかをチェック
if old_ip_ranges is None:
print('No existing IP ranges found in Firestore. Saving the initial data.')
# 初回データ保存
new_ip_ranges['lastUpdated'] = datetime.utcnow().isoformat() + 'Z'
doc_ref.set(new_ip_ranges)
return 'Initial IP ranges saved.', 200
# 差分比較
differences = find_differences(
old_prefixes=filter_data(old_ip_ranges).get("prefixes", []),
new_prefixes=filter_data(new_ip_ranges).get("prefixes", [])
)
added = differences["added"]
removed = differences["removed"]
if added or removed:
print('IP ranges have changed.')
print(f'Added IPs: {json.dumps(added, indent=2)}')
print(f'Removed IPs: {json.dumps(removed, indent=2)}')
# Firestoreは更新せずログ出力のみ実施
return 'IP ranges have changed. Check logs for details.', 200
else:
print('No changes in IP ranges.')
return 'No changes in IP ranges.', 200
except requests.exceptions.RequestException as e:
print(f'Error fetching IP ranges: {e}')
return 'Error fetching IP ranges.', 500
except Exception as e:
print(f'Error: {e}')
return 'Error detecting IP changes.', 500
functions-framework==3.*
requests
google-cloud-firestore
詳細な解説はしませんが、スクリプトの流れだけ説明します。
- 公開されているSTSのIP範囲を取得
- Firestoreからデータ取得
- Firestoreに保存されたIP範囲を取得
- Firestoreにデータが存在しない場合、取得したデータを保存(初回実行時)
- 差分の比較。新旧の prefixes を比較して差分を検出
差分がない場合:「変更なし」として処理を終了
差分がある場合:変更内容をログ出力して終了
実行してみる(初回実行)
作成したCloud Run関数を叩きます。私は認証ありにしたのでAuthorizationヘッダー
にbearerトークンを設定しています。
curl -m 70 -X POST "Cloud Run関数のURL" -H "Authorization: bearer $(gcloud auth print-identity-token)"
叩くと以下のレスポンスが返ってきます。
Initial IP ranges saved.
初回実行なのでIP範囲がFirestoreに保存されました。
Firestoreも確認してみます。
公開されている通りの値がドキュメントに設定されていることが確認できました。
続いて変更を検知できるか試してみます。
変更検知テスト(追加)
初回実行でドキュメントが作成されて2つのIP範囲が追加されています。このどちらか片方を削除してCloud Run関数を実行すれば、IP範囲が追加されたことと同義になります。
なので片方消して、Cloud Run関数を叩きます。
Firestoreのコンソールの消したい方のフィールドの右端にゴミ箱マークがあるのでそこから消せます。
削除したら叩きます。
そうすると今度は出力が少し変わります。
IP ranges have changed. Check logs for details.
IP範囲が変更されたことを検知できているのでログを見てみます。
削除した方のIP範囲がAdded IPs
として出力されていました。ちゃんと追加を検知できていますね。
変更検知テスト(削除)
リファレンスでは追加しかないようですが、一応削除された場合も検知できるか確かめてみます。
削除をテストするには、公開されているIP範囲に存在しないIP範囲をFirestoreのドキュメントに設定すればよいです。
prefixes
の右側の+
(プラス)ボタンを押下します。
フィールドを追加でmap
を選択して、フィールド名にipv4Prefix
・フィールドタイプstring
・フィールド値192.168.0.0/32
を設定します。
設定したらCloud Run関数を叩きます。叩いたらログをみます。
Removed IPs
に追加したIP範囲が出力されていることが確認できました。問題なく削除も検知できていますね。
無事意図した動作をしていてよかったです。
所感
この仕組みの使い道としては、出力ログを元にアラートを飛ばしたり、SlackやTeamsに通知をしてIP範囲の変更検知に繋げることができると思います。
ただ、あくまで検知だけなので実際に検知した場合にはS3のバケットポリシーのIP範囲を修正する必要があります。少なくとも 7 日間は待機します。
とリファレンスにあるので、その間に対応しなければならないです。
つまりは運用体制も整備しておいた方が良さそうです。
IP範囲が今後追加されるかはわかりませんが、リファレンスでもPULLしてチェックしなさいという旨の記載があるとおりこのようなチェックの仕組みを持っておいた方が安心だなと思います。
それではまた。ナマステー
参考